Mono & Flux
contents
Spring WebFlux(리액티브 프로그래밍)의 핵심입니다. 기존 Spring MVC(Blocking)에서 WebFlux(Non-blocking)로 넘어가려면 Mono와 Flux를 이해하는 것은 필수입니다.
이 둘은 리액티브 스트림 표준(Project Reactor)에서 Publisher(발행자) 인터페이스를 구현한 두 가지 주요 타입입니다.
상세 내용은 다음과 같습니다.
1. 패러다임의 전환: Pull vs. Push
- 전통적 방식 (List): 호출하고, 기다리고, 거대한 리스트를 한방에 받습니다. 마치 DVD 박스 세트를 사는 것과 같습니다. (Pull 방식)
- 리액티브 방식 (Flux/Mono): 채널을 구독(Subscribe)하면, 데이터가 시간이 지남에 따라 하나씩 도착합니다. 마치 넷플릭스를 스트리밍하는 것과 같습니다. (Push 방식)
2. Mono란 무엇인가? (0 또는 1)
Mono<T>는 최대 1개의 데이터를 발행하는 Publisher입니다.
- 비유:
CompletableFuture<T>나Optional<T>과 비슷합니다. 결과가 있거나, 아니면 실패합니다. - 사용 사례:
- ID로 사용자 한 명 조회 (
findById). - 단일 JSON 객체를 반환하는 HTTP 요청.
- 반환 값이 없는 완료 신호만 필요한 작업 (void).
- ID로 사용자 한 명 조회 (
코드 예시:
// 여기선 아무 일도 안 일어납니다! 그냥 정의일 뿐입니다.
Mono mono = Mono.just("Hello World");
// 구독(subscribe)을 해야만 데이터가 흐릅니다.
mono.subscribe(System.out::println);
3. Flux란 무엇인가? (0 ~ N)
Flux<T>는 0개에서 무한대의 데이터를 발행하는 Publisher입니다.
- 비유:
Stream<T>이나List<T>와 비슷하지만, 아이템이 시간에 따라 흐릅니다. - 사용 사례:
- 사용자 목록 전체 조회 (
findAll). - 실시간 데이터 피드 (주식 가격, 트위터 스트림).
- 대용량 파일을 한 줄씩 읽기 (메모리에 다 올리지 않고).
- 사용자 목록 전체 조회 (
코드 예시:
// 3개의 아이템이 흐르는 스트림
Flux flux = Flux.just("Apple", "Banana", "Cherry");
flux.subscribe(item -> System.out.println("Received: " + item));
4. 결정적 개념: "구독하기 전까진 아무 일도 없다"
초보자가 가장 헷갈려 하는 부분입니다. 일반 자바에서는:
List list = repository.findAll(); // 여기서 즉시 DB 호출이 발생합니다.
Reactor/WebFlux에서는:
Flux users = repository.findAll(); // 아무 일도 안 일어납니다. DB 호출 안 함.
Flux는 그저 요리법(Recipe) 이자 설계도일 뿐입니다. 실제 실행(DB 쿼리, HTTP 호출)은 누군가가 .subscribe()를 호출할 때 비로소 시작됩니다. Spring WebFlux 컨트롤러에서는 요청이 들어올 때 웹 프레임워크(Netty) 가 알아서 구독을 해줍니다.
5. 주요 연산자 (도구 상자)
자바 스트림처럼 연산자를 사용해 데이터를 가공합니다.
A. map (동기적 변환)
데이터를 1:1로 동기적으로 변환할 때 씁니다.
Mono<User>$\rightarrow$Mono<UserDto>
flux.map(user -> new UserDto(user.getName()));
B. flatMap (비동기적 변환)
변환 작업이 또 다른 비동기 호출(DB나 API 호출 등)을 포함할 때 씁니다.
- 올바른 사용:
flatMap은 내부의 비동기 호출이 실행되도록 하고 그 결과를 평평하게 폅니다. - 잘못된 사용: 여기서
map을 쓰면Mono<Mono<Details>>(중첩된 모노)가 되어버립니다.
// 각 유저에 대해 외부 API를 호출해 상세 정보를 가져옴
flux.flatMap(user -> externalService.getDetails(user.getId()));
C. zip (결합)
여러 비동기 작업을 병렬(Parallel) 로 실행하고 그 결과를 합칠 때 씁니다.
Mono userMono = repo.findUser();
Mono ordersMono = repo.findOrders();
// 둘 다 병렬로 실행하고, 둘 다 끝날 때까지 기다렸다가 합침
Mono.zip(userMono, ordersMono)
.map(tuple -> new Dashboard(tuple.getT1(), tuple.getT2()));
6. 백프레셔 (Backpressure - 안전 밸브)
이것이 Flux의 초능력입니다.
빠른 생산자(서버)가 초당 10,000개를 보내는데, 느린 소비자(클라이언트)가 초당 100개밖에 처리 못 한다고 가정해 봅시다.
- 백프레셔 없음: 소비자는 메모리 부족(
OutOfMemoryError)으로 죽습니다. - 백프레셔 있음: 소비자가 생산자에게 말합니다: "나 지금 벅차니까 일단 50개만 보내줘." 생산자는 속도를 조절합니다.
요약 테이블
| 특징 | Mono | Flux |
|---|---|---|
| 개수 (Cardinality) | 0 또는 1개 | 0 ~ N개 (무한 가능) |
| Java 대응 | CompletableFuture, Optional |
List, Stream |
| 에러 처리 | 에러와 함께 종료 | 에러와 함께 종료 |
| 백프레셔 | 해당 없음 (1개뿐이라) | 매우 중요 |
| 주요 용도 | findById, save, HTTP 응답 |
findAll, 데이터 스트림, SSE |
references